Week 1. Introduction to R (II)
Introduction to R II
이번 주는 데이터를 효율적으로, 그리고 체계적으로 전처리하는 방법에 대해서 살펴보겠습니다. 저번 주에 이어서 전처리 및 데이터 관리를 효율적이고 간편하게 수행하기 위해 tidyverse 패키지와 그 패키지에 속하는 다른 패키지들(tidyverse familiy), 그리고 함수들을 주로 사용하도록 하겠습니다.
패키지 불러오기
# install.package("pacman") # 여러 개의 패키지를 한 번에 불러올 수
# 있게끔 도와주는 패키지
# pacman::p_load(here, lubridate, ezpickr, tidyverse)
library(here) # 현재 작업디렉토리를 R-스크립트가 위치한 디렉토리로 자동설정하는 패키지
library(lubridate) # 날짜시각 데이터를 원활하게 가공하는 데 특화된 패키지
library(ezpickr) # 다른 확장자의 파일을 R로 불러오기 위한 패키지
library(tidyverse) # 데이터 관리 및 전처리를 위한 주요 패키지들어가기에 앞서서 간단한 기본 함수들을 리뷰해보도록 하겠습니다.
## [1] 15
자칫하면 위의 함수에서 x <- 10으로 x라는 객체에 10을 넣었기 때문에 10 + 10이 되어 결과를 20으로 리턴할 것이라고 생각할 수 있습니다. 하지만 어디까지나 add_one 함수가 정의되고 난 이후, x는 add_one()의 괄호 안에 들어가는 값으로 재정의되었습니다. 따라서 5를 add_one()에 투입한 순간, 그 함수는 5 + 10을 계산하게 되어 결과는 15가 됩니다.
## Error in new_add_one(): argument "x" is missing, with no default
이번에는 new_add_one() 함수를 정의하고, x 값을 따로 주지 않고 빈 함수를 작동시켰습니다. 이때, new_add_one()은 주어진 투입값(input)이 없기 때문에, 함수에 요구되는 x를 찾기 위해 함수 안에서 x를 탐색하는 것을 넘어서 한 단계 위에서 x를 찾습니다. 바로 처음에 만든 x <- 10을 불러오는 것입니다. 따라서 10 + 10, 20을 출력하게 됩니다.
그럼 여기서 함수와 객체의 관계에 대해 다시 한 번 살펴보도록 하겠습니다.
## [1] "Hello, world"
## Error in print(z): object 'z' not found
print_hello_world() 함수를 작동시키면 함수 내에서 정의된 객체 z의 값을 반환합니다. 그렇지만 그 z는 어디까지나 함수 내에서만 정의된 것이지 R의 글로벌 환경에 저장된 객체는 아닙니다. 따라서 함수 내에서 정의된 z를 출력하라고 명령하면, 오류 코드를 확인하게 됩니다.
R에서 객체를 제외하고 작동하는 모든 기능들은 ’함수’라고 부릅니다. 그리고 함수는 같은 결과를 다른 방식으로 출력할 수도 있습니다.
## [1] 10
## [1] 10
또한, 기존에 R에는 내장되지 않았던 함수도 별도로 특정하게 지정하여 만들 수 있습니다. 그러나 이 경우에는 별도의 패키지로 만들어서 저장해주지 않는 한, R 코드가 작성된 해당 세션에서만 지속되는 함수일 뿐입니다.
아래는 문자열과 문자열을 하나로 합치고 있는데, 일반적으로 두 객체를 합칠 때 쓰는 함수인 +는 숫자형 객체 간에만 기능합니다. 따라서 문자열끼리 합쳐주는 함수, paste()와 동일한 기능을 하는 별도의 함수 기호를 하나 만들어 보겠습니다.
## Error in "hello" + "world": non-numeric argument to binary operator
## [1] "hello world"
## [1] "hello world"
이제 패키지를 불러오는 작업과 간단한 함수, 그리고 객체의 특성에 대해 리뷰했으니 다음으로 넘어가 보도록 하겠습니다.
작업 디렉토리 설정하기
아까 불러온 here 패키지의 here() 함수를 사용해보도록 하겠습니다. here()를 사용하면 자동으로 현재 R-스크립트가 저장된 경로를 확인, 복사합니다. %>%는 파이프 왼쪽의 기능 이후에 오른쪽의 기능을 적용시키는 지정된 함수로 tidyverse 패키지가 가지고 있는 강점 중 하나라고 할 수 있습니다.
이 파이프 함수를 이용하여 우리는 코드를 보다 논리적으로, 그리고 정연하게 작성하여 가독성을 높일 수 있습니다.
here() %>% setwd() # here()로 R-스크립트의 디렉토리를 확인하고 난 다음에
# 그 디렉토리로 작업 디렉토리를 설정(set working directory, setwd)하라는
# 함수를 작동시킨 것입니다.이렇게 작업 디렉토리를 설정하였다면, 이제 데이터를 불러와 보겠습니다.: ggplot 패키지에 내장된 diamonds 데이터를 사용해보도록 하겠습니다. ggplot2 패키지가 로드 되어 있다면 해당 데이터는 쉽게 불러올 수 있습니다.
## [1] "tbl_df" "tbl" "data.frame"
데이터 전처리 하기(Data Cleaning)
dplyr 패키지를 이용한 데이터 전처리
데이터를 들여다보고, 결측치를 확인하기
## [1] 53940
## [1] 10
## [1] 10
## 기본적으로 R에 내장된 함수들을 이용하여 변수를 만들고 목록화하기(indexing)
## index라는 새로운 변수를 만들고 행의 수로 연번 매기기
df$index <- 1:nrow(df)
## 맨 위의 몇 개 행을 보여줍니다.
head(df) ## Rows: 53,940
## Columns: 11
## $ carat <dbl> 0.23, 0.21, 0.23, 0.29, 0.31, 0.24, 0.24, 0.26, 0.22, 0.23,...
## $ cut <ord> Ideal, Premium, Good, Premium, Good, Very Good, Very Good, ...
## $ color <ord> E, E, E, I, J, J, I, H, E, H, J, J, F, J, E, E, I, J, J, J,...
## $ clarity <ord> SI2, SI1, VS1, VS2, SI2, VVS2, VVS1, SI1, VS2, VS1, SI1, VS...
## $ depth <dbl> 61.5, 59.8, 56.9, 62.4, 63.3, 62.8, 62.3, 61.9, 65.1, 59.4,...
## $ table <dbl> 55, 61, 65, 58, 58, 57, 57, 55, 61, 61, 55, 56, 61, 54, 62,...
## $ price <int> 326, 326, 327, 334, 335, 336, 336, 337, 337, 338, 339, 340,...
## $ x <dbl> 3.95, 3.89, 4.05, 4.20, 4.34, 3.94, 3.95, 4.07, 3.87, 4.00,...
## $ y <dbl> 3.98, 3.84, 4.07, 4.23, 4.35, 3.96, 3.98, 4.11, 3.78, 4.05,...
## $ z <dbl> 2.43, 2.31, 2.31, 2.63, 2.75, 2.48, 2.47, 2.53, 2.49, 2.39,...
## $ index <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, ...
데이터를 좀 더 자세하게 들여다보기
## # A tibble: 53,940 x 11
## carat cut color clarity depth table price x y z index
## <dbl> <ord> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl> <int>
## 1 0.23 Ideal E SI2 61.5 55 326 3.95 3.98 2.43 1
## 2 0.21 Premium E SI1 59.8 61 326 3.89 3.84 2.31 2
## 3 0.23 Good E VS1 56.9 65 327 4.05 4.07 2.31 3
## 4 0.290 Premium I VS2 62.4 58 334 4.2 4.23 2.63 4
## 5 0.31 Good J SI2 63.3 58 335 4.34 4.35 2.75 5
## 6 0.24 Very Good J VVS2 62.8 57 336 3.94 3.96 2.48 6
## 7 0.24 Very Good I VVS1 62.3 57 336 3.95 3.98 2.47 7
## 8 0.26 Very Good H SI1 61.9 55 337 4.07 4.11 2.53 8
## 9 0.22 Fair E VS2 65.1 61 337 3.87 3.78 2.49 9
## 10 0.23 Very Good H VS1 59.4 61 338 4 4.05 2.39 10
## 11 0.3 Good J SI1 64 55 339 4.25 4.28 2.73 11
## 12 0.23 Ideal J VS1 62.8 56 340 3.93 3.9 2.46 12
## # ... with 53,928 more rows
결측치 확인하기
데이터 안에 결측치(NA, missing data)가 몇 개나 있는지를 확인하는 것은 중요합니다. 실제로 통계분석을 수행할 때에는 변수에 결측치가 하나라도 존재하면 최종 모형에서 그 결측치가 속한 행 전체는 분석에서 제외됩니다. 따라서 분석 모형에 있어서 표본의 수(sample size)라는 측면에서 생각해볼 때, 전체 관측치의 개수만큼이나 결측치가 얼마나 되는 것을 파악하는 것도 중요합니다.
## [1] 0
## [1] 0
df라는 tibble 을 가지고 is.na(), 즉 df 중 NA인 것들만 골라서 sum()으로 더하라는 코드입니다.
파이프 함수에서
()의 빈괄호는 앞의 데이터를 그대로 받아넘기는 기능입니다.그리고
is.na()함수는 논리형으로 “()에 들어간 객체에 결측치가 있는가?”를 묻습니다.결측치가 있으면
TRUE, 없으면FALSE로 나올 것이고, R에서TRUE = 1,FALSE = 0입니다.그렇게 나온
TRUE들의 총합을 구하면df라는 데이터 안의 결측치의 총 개수를 확인할 수 있습니다.- 그렇다면 왜 총합을 구하는 함수 내에
na.rm = T라는 옵션을 사용하지 않은 것일까요? - 보통 R에서
sum()함수는 그 객체에 결측치가 하나라도 있으면 전체 계산을NA로 반환합니다. - 그러나 이 경우에서는 파이프를 통해서
df에서 결측치/관측치를 각각 1, 0으로 변환시켰기 때문에 - 더 이상 결측치도 missing data,
NA가 아닌 숫자형으로 간주되기 때문에 바로 더할 수 있습니다.
- 그렇다면 왜 총합을 구하는 함수 내에
그렇다면 이제는 각 열마다 (변수마다) 관측치가 몇이나 있는지를 확인해보겠습니다.
## carat cut color clarity depth table price x y z
## 0 0 0 0 0 0 0 0 0 0
## index
## 0
위의 함수는 df를 map_int 함수로 넘기되, 이 함수는 만약 x라는 객체가 관측치이거든 총합을 구해 그 결과를 정수형(integer)로 반환하라는 코드입니다. 즉, 이 경우 df에 관측치가 있으면 그 관측치의 총합을 더하여 숫자로 바꾼 결과를 출력할 것입니다. 변수별로 관측치의 개수를 확인할 수 있는 함수입니다.
## carat cut color clarity depth table price x y z
## 0 0 0 0 0 0 0 0 0 0
## index
## 0
## [1] "integer"
## [1] "double"
## 만약 변수명을 따로 보기 싫다면? unname() 함수 추가
df %>% map_int( function(x) is.na(x) %>%
sum() %>% as.integer() ) %>% unname()## [1] 0 0 0 0 0 0 0 0 0 0 0
unname() 함수에 대해 조금 더 알아보겠습니다.
## one two three
## 1 2 3
## [1] 1 2 3
특정 벡터 내에 결측치의 개수가 몇 개인지를 구하는 코드를 함수로 만들어서 함수 객체의 형태로 저장해보도록 하겠습니다.
get_number_of_missings_in_vector <- function(some_vector) {
result <- some_vector %>%
is.na() %>%
sum() %>%
as.integer()
return(result)
}
get_number_of_missings_in_vector(df)## [1] 0
get_number_of_missings_in_vector() 함수는 아까 위의 함수(주어진 객체의 결측치 확인, 총합 계산, 정수형 반환)를 result라는 객체에 저장하고 반환하라는 코드를 내장하고 있습니다. 따라서 우리는 앞서와 마찬가지로 df에 결측치가 없다는 0을 반환하게 됩니다.
앞서 말했다시피 데이터프레임과 같은 형식의 자료에 NA가 있으면, R은 결측치를 제외할 때, 측치가 속한 행을 아예 삭제해버립니다.
중요한 점은 R은 단지 함수적인 프로그래밍 언어이기 때문에 그 결측치를 제거한 이후에 별도로 저장해주지 않으면 다시 불러오는 객체 df는 결측치가 제거되지 않은 원래의 형태로 다시 불려오게 됩니다. 즉, new_df <- df %>% na.omit()와 같은 식으로 재저장 해주어야만 결측치가 제거된 데이터를 가지게 됩니다.
데이터에 결측치가 있을 때, 그 결측치들을 제외하게 되면 어떻게 되는지 한 번 아래의 예제 코드로 확인해보도록 하겠습니다.
한 가지 강조할 것은 모든 dplyr 함수는 (아마 거의 모든 tidyverse 함수들은) 첫 번째로 데이터를 지정하여 파이프로 넘기면 그 이후의 파이프들은 맨 처음의 데이터를 그대로 가지고 후속 함수들을 그 데이터에 순차적으로 적용하는 과정을 거치게 된다는 것입니다.
dplyr패키지의 주요 함수들
dplyr::filter(): 필터 함수
dplyr::filter() 함수는 () 안에 설정하는 조건문에 따라서 관측치를 필터링합니다. 이때, 조건문은 논리형 연산자로 기능하는데, 조건에 따라 투입값이 참(TRUE)인지 거짓(FALSE)인지 반환합니다. R에는 dplyr 패키지 말고도 다른 filter() 함수를 가지고 있기 때문에 ::의 로딩 함수를 가지고 dplyr 패키지의 filter() 함수를 직접 불러오는 것이 오류를 줄일 수 있는 방법입니다.
## %in% 함수는 우측에 지정한 객체가 좌측에 포함되어 있냐는 것을 묻는 논리형의 기능을 수행합니다.
## %in% 함수를 자세히 알아보겠습니다.
## names_라는 객체에 세 이름이 있을 때,
names_ <- c("Sara", "Robert", "James")
## names_안에 James라는 이름이 있으면?
"James" %in% names_ ## [1] TRUE
dplyr::select(): 선택 함수
dplyr::select() 함수는 데이터 안에서 특정한 변수만을 선택하고자 할 때 사용할 수 있습니다. 데이터를 관리하고 전처리를 할 때 굉장히 유용하게 사용할 수 있는 함수이다. 예를 들어, World Development Indicators에서 전체 변수 중 필요한 변수만을 선택하여 새로운 데이터로 재저장할 수 있는 것입니다.
위의 예제에서 눈여겨볼 만한 것은 바로 세 번째 select() 함수 내에서 작동하는 everything()함수와 select_if()라는 변형 함수입니다.
- 만약
everything()함수가 없었다면 변수들의 이름을 줄줄이 나열해야 해서select()함수의 효용이 많이 떨어졌을 것입니다. - 그리고
select_if()는 조건문을 반영할 수 있습니다. -를 통해서 변수를 제외하는 여집합적 구성도 가능합니다(carat과color만 제외하는 것처럼).
select() 함수를 조금 더 자세하게 알아보겠습니다.
## 인덱싱 기능을 이용하여 열번(number of columns)을 이용해 select()를 활용해보겠습니다.
## 첫 번째부터 세 번째 변수만을 선택하겠습니다.
df %>% select(1:3) ## select() 함수는 범용성이 높다. 변수명이 어떤 글자로 시작하는지, 끝나는지,
## 혹은 어떤 글자를 포함하는지에 따라서도 select()를 적용하여 변수를 선택할수 있습니다.
df %>% select(starts_with("c"))select() 함수를 이용해서 변수를 선택-추출해내는 것 외에도 변수 이름을 변경하는 것도 가능합니다. 단, 이때 everything() 함수를 지정해주는 것을 잊어서는 안 됩니다. 왜냐하면 everything() 없이 변수명만 바꿔버리면 select()는 바꾼 그 변수들만을 출력하고 나머지 변수들은 제외해버리기 때문입니다.
물론 그렇게 select() 함수를 적용하고 별도로 저장하지 않으면 df 자체에는 변화가 없기 때문에 다시 everything()을 추가해서 코드를 작동시키고 다른 객체로 저장하면 됩니다.
- 그러면 바뀐 변수 + 바꾸지 않은 다른 변수들이
new_df라던지 다른 객체 이름으로 저장될 것입니다. - 그리고 이때, 바뀐 함수들이 먼저 오고 그 다음으로 다른 변수들이 순서대로 이어지게 됩니다.
## select()로도 변수명을 바꿀 수 있지만, rename()을 이용하면 굳이 everything() 안쓰고도
## 간단하게 할 수 있습니다. 역시 편법은 쓰는 게 아닙니다.
df %>% rename(new_depth = depth, new_color = color)## 특정한 조건을 가진 변수들만 이름을 변경할 수 있습니다.
df %>% rename_at( # rename_at()으로 조건을 특정합니다.
vars( starts_with("c") ), # "c"로 이름이 시작하는 변수들을 대상으로
function(x) str_to_upper(x) # 어떻게? 모두 변수명을 대문자로 바꿉니다.
)## rename_if() 함수를 이용하면 특정 조건을 충족하는 변수들의 이름을 변경할 수 있습니다.
## 숫자형인 변수들의 이름을 대문자로 바꿔라.
df %>% rename_if( is.numeric, str_to_upper ) dplyr::mutate(): 변수 조작 함수
dplyr::mutate()는 데이터 전처리 및 관리에서 가장 요긴하게 쓰일 함수입니다. 이 함수는 새로운 변수를 만들거나 기존 변수에 조작을 가할 때 사용합니다.
## 기존 carat 변수에 10배가 된 값을 가진 새로운 변수 carat_multiplied를 만들도록 하겠습니다.
df %>% mutate(carat_multiplied = carat * 10) 하나의 mutate() 함수 내부에 여러 줄의 코드를 통해서 순서대로 변수를 조작할 수도 있습니다.
df %>% select(carat) %>%
mutate(
caret_times_2 = carat * 2,
caret_times_2_times_2 = caret_times_2 * 2,
caret_times_2_times_2_times_3 = caret_times_2_times_2 * 3
)## mutate() 함수 역시 _at, _all, _if의 세부함수를 가집니다.
## 변수가 문자형이거든 요인형으로 바꿉니다.
df %>% mutate_if(is.character, factor) 다른 dplyr 패키지의 유용한 함수들
arrange(): 변수의 값을 정렬할 때 쓰는 함수
df 데이터에서 price라는 함수 + 나머지 다른 함수로 순서를 재정리하고, 그 다음에 price를 기준으로 변수를 정렬해보도록 하겠습니다. arrange()의 디폴트 값은 오름차순입니다. 내림차순으로 바꾸고 싶으면 desc() 함수를 사용하면 됩니다.
바로 위의 코딩은 관심있는 주요 변수를 맨 앞으로 빼고 주요 변수들이 다른 변수(price)의 크기에 따라 어떻게 나타나는지를 파악할 수 있게 해주는 코드입니다. select()를 사용하지 않고 바로 arrange()를 적용하였습니다. 예를 들어, 정치체제(민주주의/비민주주의) 변수를 앞으로 빼고 정렬 기준을 GDPpc로 하는 등 응용이 가능핳ㅂ니다.
group_by(): 집단별 묶음
group_by()를 쓰면 함수 내의 같은 변수값별로 묶인 결과를 보여줍니다. 숫자형, 문자형 모두 적용됩니다. 즉, 만약 group_by(price)로 하면 변수들이 같은 가격별로 묶여서 보일 것이고, 아래와 같이 group_by(cut)을 한다면 다이아몬드 컷팅 유형별로 분류해서 보여줄 것입니다.
유의할 점은 먼저 group_by()를 지정해주고 그 이후에 다른 함수를 사용하면 집단별로 묶인 상태에서 그 함수들이 순차적으로 적용된다는 점입니다. group_by()를 사용했을 때와 그렇지 않을 때를 구분해보도록 하겠습니다. 내림차순된 가격 변수를 기준으로 첫째 행과 둘째 행, 즉 가장 비싼 가격과 두 번째로 비싼 가격만을 잘라서(slice(1 : 2)) 보여주라는 명령어입니다.
df %>% group_by(cut) %>% # df의 cut 변수 유형별로 묶었습니다.
## 그렇게 묶인 데이터가 파이프로 넘어가고, 가격 기준 내림차순 정렬
arrange(desc(price)) %>%
## 컷팅 유형별 + 가격 기준 내림차순 중 첫 두 행만 보여주라는 코드
slice(1 : 2) 한 가지 유의해야할 점은 티블 유형에 group_by()를 적용할 경우 그 결과가 일반 티블과는 다른 특성을 가지게 된다는 것입니다.
## [1] "grouped_df" "tbl_df" "tbl" "data.frame"
보면 “grouped_df”라는 특성이 추가된 것을 확인할 수 있습니다. 티블과 group_by()를 함께 쓸 때는 ungroup() 함수를 같이 사용할 것을 추천하는데, 이는 다음과 같은 이유에서입니다.
미리 언급한 바와 같이
grouped_라는 속성이 생김으로써,group_by()가 야기할 수 있는 잠재적인 오류를 피하기 위해서 입니다.ungroup()함수를 이용하여 파이프 함수로 구성된 코드가group_by()함수가 적용된 것임을 명시적으로 줄 수 있습니다. 따라서 우리는ungroup()함수가 코드에 포함되어 있다면 해당 티블이 그룹핑된 결과일 수 있다고 바로 알 수 있습니다. 즉, 코드의 가독성과 명확성이 좋아집니다.글로벌 환경을
.Rdata객체로 저장하여 불러오거나 할 때,group_by()해놓고ungroup()안하면 기록은 남아있지 않는데 해당 티블에grouped_속성이 남아 추후 분석에 어려움이 있을 수 있습니다.
## 따라서 ungroup()을 이용하여 일반적인 티블로 다시 바꿔줍니다.
df_group <- df %>% group_by(cut) %>%
arrange(desc(price)) %>%
slice(1:2) %>%
ungroup() # 원래의 티블로 돌아와!
class(df_group)## [1] "tbl_df" "tbl" "data.frame"
또, dplyr 패키지는 count() 함수도 제공합니다. 이 함수는 데이터의 특정 변수값에 기초해 그 집단 수를 세어 줍니다. 보통 분류형 변수에 많이 사용되지만 숫자형도 적용됩니다. 얘를 들어 1부터 2만에 이르는 범주를 가지는 변수가 총 50만개의 관측치를 가지고 있다고 할 때, 1의 값은 몇 개, 15는 몇 개, 2만은 몇 개와 같은 식으로 범주화를 시켜주는 것입니다.
## group_by() 함수는 summarise() 함수와 결합될 경우 다양한 응용이 가능합니다.
## 여기서 summarise는 총계를 구하라는 것이 아니라 데이터를 요약정리해서 보여줄 수 있는
## 여러 함수들을 통칭하는 것입니다.
df %>%
group_by(cut) %>%
## 컷팅 유형별로 평균 가격을 계산하라.
summarise(price_mean = mean(price)) 마찬가지로 summarise() 함수도 _if, _at, _all과 같은 세부유형으로 분류하여 사용할 수 있습니다.
df %>%
select(cut, x, y, z) %>% # cut, x, y, z 변수만 df 티블에서 뽑아내어
group_by(cut) %>% # cut 유형별로 그룹화.
## 그리고 각 컷팅유형 별로 x, y, z의 평균을 구하도록 하겠습니다.
summarise_all(mean, na.rm = T)## 평균에 더하여 중앙값, 최소값, 최대값도 구해보도록 하겠습니다.
df %>% group_by(cut) %>%
summarise_at( # 특정한 변수인 x, y, z를 대상으로
vars(x, y, z), # list 뒤의 함수들을 적용합니다.
list(mean, median, min, max),
na.rm = T) # mean 등은 데이터에 결측치가 있으면 결측치를 반환하므로 # 결측치 제거(remove na)가 TRUE이도록 설정합니다.
## 컷팅 유형별로 그룹화한 다음에 숫자형 변수들일 경우에만 평균을 계산해 보겠습니다.
df %>% group_by(cut) %>%
summarise_if(is.numeric, mean, na.rm = T)## 동일한 코드이지만 표현식이 조금 다릅니다.
df %>% group_by(cut) %>%
summarise_if(is.numeric, function(x) mean(x, na.rm = T))데이터 결합하기(Join data)
연구를 진행하다보면 하나로 분석에 필요한 모든 변수가 포함된 데이터를 만나기란 하늘에 별 따기라는 것을 알 수 있습니다. 따라서 서로 다른 소스에서 필요한 변수들을 추출해 하나의 데이터로 구성하는, 데이터 결합 작업이 중요합니다. 보통은 머징(merging)이라고도 많이 합니다.
일단 미국의 주 이름 객체 반복추출(replacement)가 가능하도록 설정하고 총 250개의 관측치를 가지는 표본을 만들어 보겠습니다. 변수명이 state_name인 티블을 하나 만들겁니다. 250의 관측치들은 미국의 각 주 이름이 중복되어 존재하게 됩니다.
sample()함수의replace = T옵션은 상자 안에서 공을 꺼낼 때, 한 번 꺼낸 공을 다시 집어넣고 다시 꺼낼 수 있다는 것을 의미합니다.- 이렇게 반복추출된
state_name변수는 미국 각 주의 이름이 무작위로 반복추출되어 총 250개의 관측치를 가지게 됩니다.
states_df <- tibble(state_name = sample(state.name, 250, replace = T))
states_df %>% count(state_name)이번에는 state_name와 미국 주 이름의 약자를 의미하는 state_abb 변수를 만들어보겠습니다. 즉, states_table은 미국의 50개 주의 이름과 약자의 두 변수를 가지고 있는 티블입니다.
자, 이제 250개의 관측치를 갖는 state_df 티블과 50개의 관측치 값을 갖는 states_table 티블을 결합해보겠습니다. 기준은 left_join()이므로 states_df가 됩니다. 따라서 우리는 states_df의 모든 관측치를 유지한 채로 states_table의 관측치를 옮겨 붙일 것입니다.
## # A tibble: 250 x 2
## state_name state_abb
## <chr> <chr>
## 1 Georgia GA
## 2 South Dakota SD
## 3 Connecticut CT
## 4 Florida FL
## 5 South Carolina SC
## 6 South Carolina SC
## 7 New Jersey NJ
## 8 Hawaii HI
## 9 Nevada NV
## 10 Texas TX
## # ... with 240 more rows
이게 가능한 이유는 두 티블 사이에 공통의 변수, state_name이 존재하기 때문입니다. 이 경우는 자동으로 묶였지만 어떤 변수를 기준으로 그룹화할 것인지 지정해줄 수도 있습니다.
## # A tibble: 250 x 2
## state_name state_abb
## <chr> <chr>
## 1 Georgia GA
## 2 South Dakota SD
## 3 Connecticut CT
## 4 Florida FL
## 5 South Carolina SC
## 6 South Carolina SC
## 7 New Jersey NJ
## 8 Hawaii HI
## 9 Nevada NV
## 10 Texas TX
## # ... with 240 more rows
이외에도 right_join(), inner_join(), full_join(), 그리고 anti_join()과 같은 함수로 결합할 수도 있습니다. 자세한 내용은 tidyverse 패키지 중 결합(join)에 관한 내용에서 살펴볼 수 있습니다. 결합, 머징에 관한 내용은 추후 더 구체적으로 다루어 보도록 하겠습니다.
일단 예시로 anti_join() 함수가 어떻게 쓰이는지 보겠습니다. anti_join()은 대개 텍스트 분석에서 사용됩니다.
text_df <- tibble(
text = c('the fox is brown and the dog is black and the rabbit is white')
)
library(tidytext)
text_df <- text_df %>%
unnest_tokens(word, text) # text를 어절로 분해
text_df변수 재코딩(recoding)
먼저 18세부터 90세 사이의 연령 중 25개를 표본으로 뽑아보겠습니다. 이때, 반복추출이 가능하게 설정합니다.
if else()
test <- tibble(age = sample(c(18:90, NA), 25, T))
test <- test %>%
mutate(
age_bins <- if_else(
is.na(age), NA_character_, # NA_character를 써주는 이유는
if_else( # 한 벡터는 하나의 자료유형만 가질 수 있기 때문입니다.
age < 25, "Young", # 여기서 다른 값들은 모두 문자열인데, NA만 논리형입니다.
ifelse( # 따라서 문자형 NA를 가지라고 지정해주는 것입니다.
age > 75, "Old", "Middle Aged" # dplyr의 if_else()의 특징입니다.
) # R에 내장된 기본함수 ifelse()는 자료유형을
# 구분하지 못합니다.
) # if_else()는 다른 자료유형이 섞이면 error를 보여줍니다.
)
)
test원하는 결과를 얻지만 만약 변수가 많아지면 조건이 많아지면서 조건을 설정하는 것과 코드를 읽는 것이 불편하다는 단점이 있습니다.
case_when()
좀 더 코드가 읽기 편한 case_when()을 소개하겠습니다.
시각화 (Vizualization): ggplot
ggplot2에 내장되어 있는 mpg라는 데이터를 이용해서 시각화에 대한 간단한 내용을 살펴보도록 하겠습니다.
cty와 hwy는 각각 도시와 고속도로 주행에 있어서의 갤런 당 마일(mpg)을 기록한 변수입니다. displ은 리터 당 엔진의 배기량을 의미합니다. drv는 자동차의 변속기 종류를 보여줍니다: 전륜구동(front wheel, f), 후륜구동(rear wheel, r), 또는 사륜구동(four wheel, 4)로 나타냅니다. model은 자동차 모델명을 말합니다. 총 38개의 모델명이 있으며, 1999년부터 2008년 사이 매년마다 새로 출시된 모델들입니다. class는 차량 유형을 보여주는 분류형 변수입니다: 2인승, SUV, 콤팩트 등.
핵심적인 그래픽 요소들
모든 ggplot2로 그린 플롯은 3개의 주요 요소를 가집니다.
- 먼저 너무나 당연하게도 플롯의 바탕이 되는 데이터입니다.
- 둘째로 변수와 그래픽 속성 간의 관계를 나타내 주는 미학적 지도(aesthetic mappings)입니다.
- 관측치를 보여줄 수 있는 최소한 하나 이상의 층위(레이어)가 필요합니다. 이러한 층위는
geom_*이라는 함수로 나타낼 수 있습니다.
간단한 예제
ggplot(mpg, # 우리가 그리고 싶은 데이터를 주고
aes(x = displ, y = hwy)) + # 어떤 변수들을 시각화하고 싶은지 지정합니다
geom_point() # 점으로 기하학적 객체를 표현하라는 레이어를 덧붙입니다.어떻게 데이터와 미학적 지도가 ggplot() 함수에 제공되는 지 눈치채셨나요? 그리고 앞의 함수를 살펴보면 추가적인 레이어가 +로 더해진 것을 확인할 수 있습니다. 그리고 미학적 지도는 항상 aes()의 함수 형태로 특정되는 것을 알 수 있습니다.
이 플롯에 변수를 몇 개 더 집어넣어보기 위해서는 colour, shape, 그리고 size와 같은 함수들을 이용할 수 있습니다. ggplot2의 몇몇 함수들은 영국식 스펠링을 가지고 있는데, 미국식 스펠링도 허용하니까 편하게 사용하면 됩니다. 추가적인 함수들도 x와 y라는 미학적 지도 함수를 사용했던 것과 동일하게 적용하면 됩니다. aes() 안에 집어넣으면 되는 것이죠.
좀 더 익숙해지면 굳이 aes() 안에 x와 y를 지정해주지 않아도 됩니다. 왜냐면 맨 처음에 입력한 두 값은 자동으로 x와 y에 배정되기 때문입니다. 즉, aes(x = displ, y = hwy)나 aes(displ, hwy)나 동일합니다.
또한, color, shape, 그리고 size와 같은 추가적인 미학 함수들은 aes() 함수 내에서 특정되어야 합니다. 즉, x나 y 처럼 그냥 적으면 R이 알아먹지를 못합니다.
만약 특정한 시각화를 위한 변수를 고정적인 값으로 설정하고 싶다면, 다음과 같이
aes()를 따로 빼내어서 지정해주면 됩니다.
## 점의 색이 파랑색이 아니라 붉은 색이고 범례에 blue라고 쓰여진 것을 확인할 수 있습니다.
ggplot(mpg, aes(displ, hwy)) + geom_point(colour = "blue")## 이렇게 하면 범례가 없이 원하던 파랑색을 얻을 수 있습니다.
ggplot(mpg, aes(displ, hwy)) + geom_point(aes(colour = "hello"))마지막 플롯에서 붉은 색을 다시 한 번 얻게되는 이유는 그 색이 aes() 내부에 특정한 값을 지정하여 넣었을 때의 디폴트 색이기 때문입니다. 즉, 어떨 때 사용할 수 있냐면, 레이어를 겹겹히 쌓으면서 서로 다른 값을 보여주는 점들을 하나의 플롯에 다양하게 보여주고 싶을 때, var1, var2 이런 식으로 geom_point(aes(color = "var1")) 이런 식으로 더하면 바로 범례에 추가하면서 서로 다른 색의 다른 변수를 보여주는 플롯을 그릴 수 있을 것입니다.
분류형 변수의 서로 다른 수준 간의 차이를 보여주는 데에는 직접 플롯에 color나 size를 조정하는 것 말고도 facets을 이용하는 방법도 있습니다. facet_wrap()이나 facet_grid()를 사용할 수 있는데, facet_wrap()이 조금 더 유용합니다.
ggplot 플롯에 더할 수 있는 다양한 유형의 geom_* 함수들로는, geom_point(), geom_bar() (또는 geom_col()), geom_path() (또는 geom_line()), 부드러운 맞춤 선을 그려주는 geom_smooth(), 그리고 geom_boxplot() 등이 있습니다.
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
geom_smooth() # 자동으로 국소가중선형회귀(Locally Weighted Scatterplot Smoother, LOWESS) 곡선을 추정해서 그려줍니다.선형 모델을 그리는 데에도 사용할 수 있습니다.
lm_plot <- ggplot(mpg, aes(displ, hwy)) +
geom_point(color = "#bd574e") +
geom_smooth(method = 'lm', color = "#ffad87") +
theme_bw()
lm_plot # 위의 컬러코드에 대해서는 다음을 참고하세요: colorhunt.co이게 익숙해지면 제곱항과 같이 자신의 함수를 다양하게 구성해서 ggplot 함수에 투입, 플롯으로 나타낼 수 있습니다.
## 함수를 직접 투입해보겠습니다.
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
geom_smooth(method = "lm", formula = y ~ I(x^2)) +
ggthemes::theme_gdocs() ## ggthemes 패키지는 다양한 테마를 제공합니다.MASS 패키지 등을 이용하여 이탈치(outliers)의 영향을 받지 않는 로버스트 선형모델을 사용하여 플롯으로 보여줄 수 있습니다.
library(MASS)
rlm_plot <- ggplot(mpg, aes(displ, hwy)) +
geom_point() +
geom_smooth(method = "rlm") +
ggthemes::theme_pander()
rlm_plot일반선형회귀모델의 결과와 로버스트 선형회귀모델의 결과를 나란히 보여주어 비교하기 편하게 할 수 있습니다. patchwork 패키지는 ggplot의 플롯들을 하나로 결합시켜주는 유용한 패키지입니다.
# install.packages('devtools')
# devtools::install_github("thomasp85/patchwork")
library(patchwork)
lm_plot + rlm_plot + plot_layout(ncol = 1)지터(Jitter) 플롯과 박스플롯
다시 데이터로 돌아와서, 만약에 우리가 자동차 구동방식에 따른 연료의 가성비에 관심이 있다고 해보겠습니다. 산포도를 그려볼 수 있겠죠?
뭐랄까, 그다지 정보를 파악하는 데 유용해보이지는 않습니다. 왜냐하면 저 점들이 하나의 관측치가 아니라 같은 값들을 가진 관측치들이 “겹쳐있기” 때문입니다. 관측치들의 집중도를 살펴보기 위해서는 투명도를 조정해주면 됩니다.
혹은 hex를 이용해 농도로 관측치들의 집중도를 나타내줄 수 있습니다.
하지만 보다 일반적으로 사용되는 방법은 지터 플롯을 이용하는 것입니다. 지터 플롯은 무작위 노이즈를 값에 더해서 점들이 미묘하게 다른 값을 가지게 해 산포도를 알아보기 쉽게 “옆으로” 퍼트리는 것이라고 볼 수 있습니다.
자동차 구동방식에 따라 나타내고자 하였기 때문에 알아보기 쉽게 색을 더해보도록 하겠습니다.
이와 유사한 정보를 주는 다른 종류의 플롯으로는 박스플롯과 바이올린 플롯이 있습니다. 참고로 박스플롯의 안에 그려진 선은 평균이 아니라 중앙값(median)입니다.
box <- ggplot(mpg, aes(drv, hwy)) + geom_boxplot() + theme_bw()
violin <- ggplot(mpg, aes(drv, hwy)) + geom_violin() + theme_bw()
box + violin + plot_layout(ncol = 2)히스토그램과 폴리곤 플롯도 어렵지 않게 ggplot으로 그릴 수 있습니다. 박스플롯이 서로 다른 두 변수 간의 관계를 보여주는 데 유용하다면, 히스토그램과 빈도폴리곤은 하나의 연속형 변수의 분포를 보여줄 때 사용합니다.
hist <- ggplot(mpg, aes(hwy)) + geom_histogram() + theme_bw()
poly <- ggplot(mpg, aes(hwy)) + geom_freqpoly() + theme_bw()
dens <- ggplot(mpg, aes(hwy)) + geom_density() + theme_bw()
hist + poly + dens + plot_layout(ncol = 1)만약 원한다면 구간(binning)을 특정해줄 수도 있습니다.
연속형 변수를 대상으로 histogram 등을 사용한다면, 이번에는 이산형 변수의 분포를 알아볼 때 유용한 막대플롯을 살펴보겠겠습니다. 당연히 geom_bar() 함수를 사용합니다.
ggplot(mpg, aes(manufacturer)) +
geom_bar() +
scale_x_discrete(guide = guide_axis(n.dodge=3))+
theme_bw() 막대 플롯과 같은 경우, 우리가 가진 데이터를 요약해서 보여주지만, 요약된 자료 그 자체를 가지고 바로 막대플롯으로도 만들 수 있습니다. 예를 들어, 아래와 같은 데이터가 있다고 해보겠습니다.
만약 이 경우, 우리는 ggplot에게 “이미 이 데이터는 요약된 정보를 가진 데이터야” 라는 것을 알려주기 위해 stat = "identity" 라는 특성을 제공합니다.
막대 플롯을 그리는 데에는 geom_col()도 이용할 수 있습니다/
라인 플롯은 관측치—점들을 왼쪽에서 오른쪽으로 이어주는 플롯이고, 반대로 경로 플롯(path plot)은 데이터셋에 나타나는 순서대로 이어주는 플롯입니다. 즉, 라인 플롯은 x의 값대로 정렬된 형태의 경로 플롯이라고 이해하실 수 있습니다.
## x 값을 정렬한 경로 플롯입니다.
temp_data %>%
arrange(x) %>%
ggplot(aes(x = x, y = y)) + geom_path() + theme_bw()축 수정(Modifying Axes)
축의 제목을 수정하기 위해서는 각각의 축에 대해 xlab(), ylab()을 사용하거나 혹은 labs()를 이용해 한 번에 수정하는 방법이 있습니다.
ggplot(mpg, aes(cty, hwy)) +
geom_point(alpha = 1 / 3) +
xlab("city driving (mpg)") +
ylab("highway driving (mpg)") +
theme_bw()# 축 제목을 모두 없애보겠습니다/
ggplot(mpg, aes(cty, hwy)) +
geom_point(alpha = 1 / 3) +
xlab(NULL) +
ylab(NULL) +
theme_bw()# labs()를 이용해서 한 번에 간편하게 할 수도 있습니다.
ggplot(mpg, aes(cty, hwy)) +
geom_point(alpha = 1 / 3) +
labs(x = "city driving (mpg)", y = "highway driving (mpg)") +
theme_bw()각 축의 값의 범주를 조정할 수도 있습니다. 이때 필요한 함수들은 xlim(), ylim(), 그리고 lims() 입니다.
## 구동 종류를 두 가지로 줄이고, hwy도 20부터 30으로 값을 줄이겠습니다.
ggplot(mpg, aes(drv, hwy)) +
geom_jitter(width = 0.25) +
lims(x = c("f", "r"), y = c(20, 30)) +
theme_bw()## 연속형 변수일 경우, 한쪽만 제한(상한선, 하한선)하고 싶을 때는 NA를 이용합니다. 아래는 상한만 30으로 설정한 경우입니다.
ggplot(mpg, aes(drv, hwy)) +
geom_jitter(width = 0.25, na.rm = TRUE) +
ylim(NA, 30) + theme_bw()## 만약 플롯의 특정한 부분만 자세히 살펴보려면 coord_cartisian()이라는 함수를 이용할 수 있습니다.
ggplot(mpg, aes(drv, hwy)) +
geom_jitter(width = 0.25) +
coord_cartesian(y = c(20, 30)) + theme_bw()여기서 한 가지 팁은 directlabels라는 패키지입니다. 이 패키지를 이용하면 범례를 이용하지 않고도 더 깔끔한 여러 가지 색상을 이용할 수 있습니다.
# install.packages('directlabels')
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() + theme_bw()ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point(show.legend = FALSE) +
directlabels::geom_dl(aes(label = class), method = "smart.grid") +
theme_bw()플롯에 우리가 필요로 하는 정보를 기록할 수 있습니다. annotation이라고 하는데요.
`geom_text()는 텍스트를 직접 플롯에 덧붙이거나 혹은 특정 한 점 관측치에 레이블을 새길 때 주로 사용합니다. 이탈치나 어떤 중요한 의미를 가지는 관측치에 한에서 레이블을 달아주면 플롯에 이해가 쉬워집니다.
geom_rect()은 플롯에서 관심있는 부분을 사각형의 형태로 하이라이트 해줄 수 있습니다.geom_rect()는xmin,xmax,ymin,ymax와 같은 미학요소들을 이용해서 사각형을 그릴 수 있습니다.geom_line(),geom_path(), 그리고geom_segment()는 선을 덧그리는 데 사용됩니다.- 이 세
geom함수들은 모두 화살표 요소를 가지고 있어서 선 끝에 화살표를 달 수 있습니다.arrow()함수가 바로 그것입니다.angle(),length(),end(),type()등으로 다양한 길이와 모양의 화살표를 그릴 수 있습니다.
- 이 세
geom_vline(),geom_hline(), 그리고geom_abline()은 일종의 기준선을 그릴 수 있게 도와줍니다.
## Rows: 574
## Columns: 6
## $ date <date> 1967-07-01, 1967-08-01, 1967-09-01, 1967-10-01, 1967-11-0...
## $ pce <dbl> 506.7, 509.8, 515.6, 512.2, 517.4, 525.1, 530.9, 533.6, 54...
## $ pop <dbl> 198712, 198911, 199113, 199311, 199498, 199657, 199808, 19...
## $ psavert <dbl> 12.6, 12.6, 11.9, 12.9, 12.8, 11.8, 11.7, 12.3, 11.7, 12.3...
## $ uempmed <dbl> 4.5, 4.7, 4.6, 4.9, 4.7, 4.8, 5.1, 4.5, 4.1, 4.6, 4.4, 4.4...
## $ unemploy <dbl> 2944, 2945, 2958, 3143, 3066, 3018, 2878, 3001, 2877, 2709...
## 시간의 흐름에 따른 미국의 실업자 수의 변화를 나타내보겠습니다.
ggplot(economics, aes(date, unemploy)) +
geom_line() + theme_bw()## 미국 대통령의 재임기간입니다.
presidential <- presidential %>%
dplyr::filter(start > economics$date[1])
presidential## 이 두 가지 자료를 합해보면 다음과 같습니다.
ggplot(economics) +
geom_rect(
aes(xmin = start, xmax = end, fill = party),
ymin = -Inf, ymax = Inf, alpha = 0.2,
data = presidential
) +
geom_vline(
aes(xintercept = as.numeric(start)),
data = presidential,
colour = "grey50", alpha = 0.5
) +
geom_text(
aes(x = start, y = 2500, label = name),
data = presidential,
size = 3, vjust = 0, hjust = 0, nudge_x = 50
) +
geom_line(aes(date, unemploy)) +
scale_fill_manual(values = c("blue", "red")) +
theme_bw() + theme(legend.position = "top")우리는 geom을 이용해서 데이터 하나를 가지고 여러 가지 층위를 덧씌워 다양한 정보를 가진 플롯으로 재생산할 수 있습니다.
이제까지는 개별 geom을 하나씩 적용해서 살펴보았습니다만, 이 geom들을 하나의 ggplot 내에서 동시에 집합적으로 사용할 수 있습니다.
척도, 범례
척도는 축과 범례를 만드는 도구로, 데이터를 어떠한 단위로 표현할 것인가에 사용됩니다.
ggplot(mpg, aes(displ, hwy)) +
geom_point(aes(colour = class)) +
scale_x_continuous(breaks = c(seq(0, 7, 0.5))) +
scale_y_continuous() +
scale_color_discrete() + theme_bw()
scale 함수를 통해서 원하는 축 제목을 바로 넣을 수도 있습니다.
ggplot(mpg, aes(displ, hwy)) +
geom_point(aes(colour = class)) +
scale_x_continuous("A really awesome x axis ") +
scale_y_continuous("An amazingly great y axis ") + theme_bw()
geom과는 다르게 scale에서 +를 사용하게 되면 이전에 사용한 scale을 덮어쓰게 됩니다.
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
scale_x_continuous("Label 1") +
scale_x_continuous("Label 2") + theme_bw()## 서로 다른 스케일을 동시에 사용할 수도 있습니다.
ggplot(mpg, aes(displ, hwy)) +
geom_point(aes(colour = class)) +
scale_x_sqrt() +
scale_colour_brewer() + theme_bw()scale은 _를 이용해서 세부 함수들이 구성되어 있습니다. 크게 세 부분으로 나뉘는데, 1. scale 2. scale을 적용할 미학 요소의 이름 (예, colour, shape 또는 x) 3. scale의 이름 (예, continuous, discrete, brewer).
df <- data.frame(x = 1:2, y = 1, z = "a")
p <- ggplot(df, aes(x, y)) + geom_point()
p + scale_x_continuous("X axis")한편, ggplot에서는 직접적으로 범례를 건드리지는 않습니다. 대신에 데이터를 수정해서 애초에 범례가 원하는 대로 깔끔하게 나오도록 하는 것이 일반적입니다.
우리는 scales 패키지를 이용해서 축에 레이블을 다양한 척도로 매길 수 있습니다.
scales::comma_format()큰 숫자에 자동으로 콤마를 넣어줍니다.scales::unit_format(unit, scale)은 원래의scale에 단위를 넣어줍니다/scales::dollar_format(prefix, suffix)은 현재의 값을 소수점 둘째 자리로 반올림해서 달러로 표현해줍니다.scales::wrap format()은 길이가 긴 레이블을 몇 개의 줄로 줄바꿈해줍니다.- axes + scale_y_continuous(labels = scales::percent_format())
- axes + scale_y_continuous(labels = scales::dollar_format(prefix = “$”))
테마
마지막으로 테마 함수는 데이터가 아닌 플롯의 요소들을 관리하는 데 사용됩니다. 플롯의 배경색, 범례 위치, 제목 크기, 글꼴 등을 변경하는 데 사용됩니다.
ggplot(mpg, aes(cty, hwy, color = factor(cyl))) + geom_jitter() +
geom_abline(colour = "grey50", size = 2) +
labs(
x = "City mileage/gallon",
y = "Highway mileage/gallon",
colour = "Cylinders",
title = "Highway and city mileage are highly correlated"
) +
scale_colour_brewer(type = "seq", palette = "Spectral") +
theme_bw() +
theme(
plot.title = element_text(face = "bold", size = 12),
legend.background =
element_rect(fill = "white", size = 4, colour = "white"),
legend.justification = c(0, 1),
legend.position = c(0, 1),
axis.ticks = element_line(colour = "grey70", size = 0.2),
panel.grid.major = element_line(colour = "grey70", size = 0.2),
panel.grid.minor = element_blank()
) 기존에 이미
ggplot이 제공하는 테마로는 theme_bw(), theme_linedraw(), theme_light(), theme_dark(), theme_minimal(), theme_classic(), 그리고 theme_void() 등이 있습니다. 이외에도 ggthemes와 같은 패키지를 통해 추가로 다른 theme을 찾아볼 수 있습니다.
마지막으로 theme의 개별 요소들을 수정하기 위해서는 다음과 같은 코드를 사용합니다: plot + theme(element.name = element function())